using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Data;

#if !PocketPC
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
#endif

namespace ODX.Core
{
#if !PocketPC
    [Serializable]
#endif
    /// <summary>
    /// Controls the following: proxy classes generation, proxy objects instantiation, client data cache, data provider calls.
    /// </summary>
    public class Session : ICloneable
    {
        internal const string SessionKey = "_SESSION";
        internal const string TypeDefField = "TypeDef";

        private DataSet schema;
        [NonSerialized] private IDataProvider dataProvider;
        private Polymorpher pm;
        private Hashtable loadedRelations = new Hashtable();
        private Stack savePoints = new Stack();
        [NonSerialized] private WeakEntityStorage entityCache;

        private WeakEntityStorage EntityCache
        {
            get
            {
                if ( entityCache == null )
                    entityCache = new WeakEntityStorage();
                return entityCache;
            }
        }

        /// <summary>
        /// This is the heart of Session object - schema & data storage.
        /// This is the <see cref="DataSet"/> which is used to store client data and perform local searches.
        /// </summary>
        public DataSet Schema
        {
             get
             {
                 if (schema == null)
                 {
                     schema = new DataSet();
                     schema.EnforceConstraints = false;
                 }
                 return schema;
             }
            set
            {
                schema = value;
                schema.EnforceConstraints = false;
            }
        }
        internal Polymorpher Pm { get { return pm; } }

        /// <summary>
        /// This is a door to the data source. Session calls it ot retrieve and save data.
        /// </summary>
        internal IDataProvider DataProvider
        {
            get { return dataProvider; }
            set { dataProvider = value; }
        }

        /// <summary>
        /// Creates disconnected session. It is used to implement disconnected mode of work
        /// and let developer only save complete session state into a binary file.
        /// </summary>
        public Session() : this(null, null) { }

        /// <summary>
        /// Creates connected session. this session mey be used fir fully featured ODX provider access.
        /// </summary>
        /// <param name="provider"></param>
        public Session(IDataProvider provider) : this(provider, null){}

        /// <summary>
        /// Creates a ready-to-use offline session.
        /// </summary>
        /// <param name="modelAssembly">Assembly name where persistent types are defined.</param>
        public Session(Assembly modelAssembly) : this(null, modelAssembly){}

        /// <summary>
        /// Creates a ready-to-use connected session.
        /// </summary>
        /// <param name="provider">Data provider for the session.</param>
        /// <param name="modelAssembly">Assembly name where persistent types are defined.</param>
        public Session(IDataProvider provider, Assembly modelAssembly)
        {
            dataProvider = provider;
            pm = new Polymorpher();
            schema = new DataSet();
            Schema.EnforceConstraints = false;
            Schema.ExtendedProperties[SessionKey] = this;

            if (modelAssembly != null)
            {
                RegisterAssembly(modelAssembly);
                Prepare();
            }
        }

        /// <summary>
        /// Finishes entity registrations and creates DataSet objects which is returned by Session.Schena property.
        /// </summary>
        public void Prepare()
        {
#if !PocketPC
            schema = DataSetBuilder.BuildDataSet(pm);
#endif
            Schema.EnforceConstraints = false;
            Schema.ExtendedProperties[SessionKey] = this;
            foreach (DataTable dt in Schema.Tables)
            {
                dt.ColumnChanged += new DataColumnChangeEventHandler(dt_ColumnChanged);
                dt.ColumnChanging += new DataColumnChangeEventHandler(dt_ColumnChanging);
                dt.RowDeleted += new DataRowChangeEventHandler(dt_RowDeleted);
                dt.RowDeleting += new DataRowChangeEventHandler(dt_RowDeleting);
            }
#if !PocketPC
            ProxyBuilder.BuildProxy(this);
#endif
        }

        ///<summary>
        /// Finishes entity registrations and creates DataSet objects which is returned by Session.Schena property.
        ///</summary>
        [Obsolete("Use Session.Prepare() instead")]
        public void BuildDataSet()
        {
            Prepare();
        }

        void dt_RowDeleting(object sender, DataRowChangeEventArgs e)
        {
            Entity entity = FindEntity(e.Row["ID"].ToString());
            if (entity != null)
                entity.OnDeleting(e);
        }

        void dt_RowDeleted(object sender, DataRowChangeEventArgs e)
        {
            Entity entity = FindEntity(e.Row["ID", DataRowVersion.Original].ToString());
            if (entity != null)
                entity.OnDeleted(e);
        }

        void dt_ColumnChanging(object sender, DataColumnChangeEventArgs e)
        {
            Entity entity = FindEntity(e.Row["ID"].ToString());
            if (entity != null)
                entity.OnChanging(e);
        }

        void dt_ColumnChanged(object sender, DataColumnChangeEventArgs e)
        {

            Entity entity = FindEntity(e.Row["ID"].ToString());
            if (entity != null)
                entity.OnChanged(e);
        }

        internal static string CreateID()
        {
            return Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Substring(0, 22);
        }

        /// <summary>
        /// Defines wheather Session contains changed objects.
        /// </summary>
        /// <returns></returns>
        public bool HasChanges()
        {
            return Schema.HasChanges();
        }

        /// <summary>
        /// Sends local Session.Schema to the data provider (Session.Data).
        /// Supports a number of save modes. See enum SaveMode
        /// </summary>
        /// <param name="saveMode"></param>
        public void Save(SaveMode saveMode)
        {
            DataSet changes = null;

            foreach (DataTable dt in schema.Tables)
                if (dt.Columns.Contains(SpecialColumns.RowVersion))
                    foreach (DataRow dr in dt.Rows)
                        if ( dr.RowState == DataRowState.Added || dr.RowState == DataRowState.Modified )
                            dr[SpecialColumns.RowVersion] = CreateID();

            switch (saveMode)
            {
                case SaveMode.Complete:
                    changes = schema.Copy();
                    break;

                case SaveMode.Concurrent:
                case SaveMode.Overwrite:
                    changes = Schema.GetChanges();
                    break;
            }

            if (changes != null)
                dataProvider.Save(changes, saveMode);
            Schema.AcceptChanges();
            savePoints.Clear();
        }
        /// <summary>
        /// Saves changes using default ADO.NET algorithm:
        /// Gets changes, then creates DataAdapter and calls it's Update() method.
        /// </summary>
        public void Save()
        {
            Save(SaveMode.Concurrent);
        }

        /// <summary>
        /// Adds data form external DataSet to the session.
        /// This method is used to register manually loaded data in the Session.
        /// </summary>
        /// <param name="ds"></param>
        public void Merge(DataSet ds)
        {
            // merge algorithm - update unchanged only
            DataSet changes = Schema.GetChanges();
            Schema.Merge(ds, false, MissingSchemaAction.Ignore);
            if (changes != null)
                Schema.Merge(changes, false, MissingSchemaAction.Ignore);
            Schema.ExtendedProperties[SessionKey] = this;

            FixupReferences();
        }

        /// <summary>
        /// Registers data in the Session.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ds"></param>
        /// <returns> Entities that originally were in the merging DataSet</returns>
        public ICollection<T> Merge<T>(DataSet ds) where T : Entity
        {
            Merge(ds);
            string tableName = Pm.GetTypeTable(typeof(T));
            return CreateEntityList<T>(tableName, ds);
        }

        /// <summary>
        /// Registers Entity of another session in the current session.
        /// /// </summary>
        /// <param name="e"></param>
        /// <returns>New Entity object, connected to the current session.</returns>
        public Entity Merge(Entity e)
        {
            Schema.Merge(e.Rows);
            return Entity.CreateWrapper(this, e.GetType(), e.ID);
        }

        /// <summary>
        /// Merges the whole Session DataSet in the current Session.
        /// </summary>
        /// <param name="s"></param>
        public void Merge(Session s)
        {
            Merge(s.Schema);
        }

        /// <summary>
        /// Merges data and handles unique keys in a way similar to primary keys
        /// instead of throwing a Constraint Violation Exception
        /// </summary>
        /// <param name="session"></param>
        public void Combine(Session session)
        {
            SmartDataSetMerger.Merge(Schema, session.Schema);
            Schema.ExtendedProperties[SessionKey] = this;
            FixupReferences();
        }

        internal DataRow FindRecord(string tableName, string ID)
        {
            DataRow dr = Schema.Tables[tableName].Rows.Find(ID);
            if (dr == null)
            {
                RefreshRecord(ID, tableName);
                return Schema.Tables[tableName].Rows.Find(ID);
            }

            return dr;
        }

        internal void RefreshRecord(string ID, string tableName)
        {
            DataSet update = dataProvider.GetRecord(tableName, ID);
            Merge(update);
        }

        /// <summary>
        /// returns All Entities of the specified type, that are available in the data source
        /// (or the session if disconnected).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public ICollection<T> All<T>() where T : Entity
        {
            return Select<T>(null, null, 0);
        }

        /// <summary>
        /// returns All Entities of the specified type, that are available in the data source
        /// (or the session if disconnected).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public ICollection<T> All<T>(string sort) where T : Entity
        {
            return Select<T>(null, sort, 0);
        }

        /// <summary>
        /// Selects a single object of specified type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T SelectOne<T>() where T : Entity
        {
            return SelectOne<T>(null, null);
        }

        /// <summary>
        /// Selects a single (first) object, that satisfies filter condition.
        /// </summary>
        /// <typeparam name="T">Type of <see cref="Entity"/> to be created.</typeparam>
        /// <param name="filter">Filter the creating object is fit to.</param>
        /// <returns>Entity of the specified type or <c>null</c> if not found.</returns>
        public T SelectOne<T>(string filter) where T : Entity
        {
            return SelectOne<T>(filter, null, null);
        }

        /// <summary>
        /// Selects a single (first) object, that satisfies filter condition.
        /// </summary>
        /// <typeparam name="T">Type of <see cref="Entity"/> to be created.</typeparam>
        /// <param name="filter">Filter the creating object is fit to.</param>
        /// <param name="sort">List of columns to specify sort order.</param>
        /// <param name="parameters">Parameters to be used instead of '?' placeholkders in the filter.</param>
        /// <returns>Entity of the specified type or <c>null</c> if not found.</returns>
        public T SelectOne<T>(string filter, string sort, params object[] parameters) where T : Entity
        {
            ICollection<T> coll = Select<T>(filter, sort, 1, parameters);
            if ( coll.Count == 0 )
                return null;

            IEnumerator e = coll.GetEnumerator();
            e.Reset();
            e.MoveNext();
            return (T) e.Current;
        }
        
        /// <summary>
        /// Queries the data provider (or client cache if disconnectd <see cref="Session"/>)
        /// </summary>
        /// <typeparam name="T">Type of <see cref="Entity"/> to be created.</typeparam>
        /// <param name="filter">Filter the creating object is fit to.</param>
        /// <param name="sort">List of columns to specify sort order.</param>
        /// <param name="count">Number of objects to be returned.</param>
        /// <param name="parameters">Parameters to be used instead of '?' placeholkders in the filter.</param>
        /// <returns>Collection of entities queried from the data provider.</returns>
        public ICollection<T> Select<T>(string filter, string sort, int count, params object[] parameters) where T : Entity
        {
            string tableName = Pm.GetTypeTable(typeof(T));


            if (dataProvider != null)
                Merge(dataProvider.Select(tableName, filter, sort, count, parameters));

            if (parameters != null && filter != null)
                filter = FilterParser.Parse(filter, delegate(int n) { return DataSetValueFormatter.FormatValue(parameters[n]); });

            List<T> list = new List<T>();
            ICollection Rows;
            Rows = Schema.Tables[tableName].Select(filter, sort);
            foreach (DataRow dr in Rows)
            {
                T e = Entity.CreateWrapper(dr) as T;
                if (e != null)
                    list.Add(e);
                if (count > 0 && list.Count == count)
                    break;
            }

            return list;
        }

        private ICollection<T> CreateEntityList<T>(string tableName, DataSet update) where T : Entity
        {
            List<T> list = new List<T>();
            if (update.Tables.Contains(tableName))
            {
                foreach (DataRow dr in update.Tables[tableName].Rows)
                {
                    T e = Entity.CreateWrapper(Schema.Tables[tableName].Rows.Find(dr["ID"])) as T;
                    if (e != null)
                        list.Add(e);
                }
            }
            return list;
        }

        private void FixupReferences()
        {
            foreach (Entity e in EntityCache.All)
                e.FixupReferences();
        }

        /// <summary>
        /// Creates an <see cref="Entity"/> of specified type.
        /// </summary>
        /// <typeparam name="T"><see cref="Entity"/>Entity subclass.</typeparam>
        /// <returns>Newly created <see cref="Entity"/></returns>
        public T Create<T>() where T : Entity
        {
            return (T)Entity.CreateWrapper(this, Pm.SpecifyType(typeof(T)), null);
        }

        /// <summary>
        /// register an abstract <see cref="Entity"/> subclass to be proxied.
        /// </summary>
        /// <param name="type">An abstract <see cref="Entity"/> subclass.</param>
        public void RegisterType(Type type)
        {
            pm.RegisterType(type);
        }

        /// <summary>
        /// registers all suitable classes to be proxied.<seealso cref="ManualAttribute"/>
        /// </summary>
        /// <param name="assembly">an assmbly to be queried for Entity subclasses.</param>
        public void RegisterAssembly(Assembly assembly)
        {
            foreach (Type type in assembly.GetTypes())
                if (type.IsSubclassOf(typeof(Entity)))
                    RegisterType(type);
        }

        internal static Session GetSession(DataRow row)
        {
            return (Session)row.Table.DataSet.ExtendedProperties[SessionKey];
        }

        internal void PreloadRelation(string parentID, DataRelation dataRelation)
        {

            string key = parentID + "." + dataRelation.RelationName;
            if (loadedRelations.ContainsKey(key))
                return;

            DataRow parentRow = dataRelation.ParentTable.Rows.Find(parentID);

            if (parentRow.RowState == DataRowState.Added)
            {
                loadedRelations[key] = true;
                return;
            }

            if (dataProvider != null)
            {
                DataSet ds = dataProvider.GetChildren(
                    dataRelation.ChildTable.TableName,
                    dataRelation.ChildColumns[0].ColumnName,
                    parentID);

                Merge(ds);
            }

            loadedRelations[key] = true;
        }

        internal void PreloadAssociaton(string parentID, DataRelation p2l, DataRelation l2ch)
        {
            string parentColumn = p2l.ChildColumns[0].ColumnName;
            string childColumn = l2ch.ChildColumns[0].ColumnName;
            string parentTable = p2l.ParentTable.TableName;
            string childTable = l2ch.ParentTable.TableName;
            string linkTable = l2ch.ChildTable.TableName;

            string key = parentID +
                parentTable + "." +
                linkTable + "." +
                childTable + "." +
                parentColumn + "." +
                childColumn;

            if (loadedRelations.ContainsKey(key))
                return;

            DataRow parentRow = p2l.ParentTable.Rows.Find(parentID);
            if (parentRow.RowState == DataRowState.Added)
            {
                loadedRelations[key] = true;
                return;
            }

            if (dataProvider != null)
            {
                DataSet ds = dataProvider.GetAssoc(
                    linkTable,
                    childTable,
                    parentColumn,
                    childColumn,
                    parentID);

                Merge(ds);
            }

            loadedRelations[key] = true;
        }

        internal void RegisterEntity(Entity e)
        {
            EntityCache[e.ID] = e;
        }

        /// <summary>
        /// Searche for an <see cref="Entity"/> into the session entity cache.
        /// </summary>
        /// <param name="ID">Identifier of the searched <see cref="Entity"/></param>
        /// <returns>Found <see cref="Entity"/> or <c>null</c> if not found.</returns>
        public Entity FindEntity(string ID)
        {
            return EntityCache[ID];
        }

#if !PocketPC

        private static void SaveWorkToStream(Stream s, DataSet work)
        {
            BinaryFormatter f = new BinaryFormatter();
            f.Serialize(s, work);
        }

        private void RestoreWorkFromStream(Stream s)
        {
            BinaryFormatter f = new BinaryFormatter();
            DataSet ds = (DataSet)f.Deserialize(s);
            Merge(ds);
            FixupReferences();
        }

        /// <summary>
        /// Saves current state into a stack of savepoints.
        /// </summary>
        public void SavePoint()
        {
            MemoryStream ms = new MemoryStream();
            DataSet changes = Schema.GetChanges();
            if ( changes != null )
            {
                SaveWorkToStream(ms, changes);
                savePoints.Push(ms.ToArray());
            }
            else
                savePoints.Push(new byte[0]);
        }

        /// <summary>
        /// Restores a previously saved savepoint.
        /// All objects created or queried after the savepoint was created are detached from the session.
        /// </summary>
        public void RestorePoint()
        {
            if (savePoints.Count > 0)
            {
                Schema.RejectChanges();
                byte[] save = (byte[])savePoints.Pop();
                if (save.Length != 0)
                {
                    MemoryStream ms = new MemoryStream(save);
                    RestoreWorkFromStream(ms);
                }
            }
        }

        /// <summary>
        /// Deletes savepoint from the savepoints stack.
        /// </summary>
        public void DeletePoint()
        {
            savePoints.Pop();
        }

        /// <summary>
        /// Saves current state to file
        /// </summary>
        /// <param name="filename">Path to the file.</param>
        public void SaveToFile(string filename)
        {
            using (FileStream fs = File.Create(filename))
                SaveWorkToStream(fs, Schema);
        }

        /// <summary>
        /// Saves current state to file
        /// </summary>
        /// <param name="stream">Stream to write dada into.</param>
        public void SaveToStream(Stream stream)
        {
            SaveWorkToStream(stream, Schema);
        }

        /// <summary>
        /// Loads current state from the file.
        /// All objects created or queried after the state was saved to a file
        /// are detached from the session. Accessing such object fails.
        /// </summary>
        /// <param name="filename">Path to the file</param>
        public void LoadFromFile(string filename)
        {
            using (FileStream fs = File.OpenRead(filename))
                RestoreWorkFromStream(fs);
        }

        /// <summary>
        /// Loads current state from a stream.
        /// All objects created or queried after the state was saved to a file
        /// are detached from the session. Accessing such object fails.
        /// </summary>
        /// <param name="stream">Stream to read data from.</param>
        public void LoadFromFile(Stream stream)
        {
            RestoreWorkFromStream(stream);
        }

#endif
        /// <summary>
        /// Refreshes entire session with the data source values.
        /// Refresh is lazy - it reads data only if they are accessed.
        /// </summary>
        public void Refresh()
        {
            loadedRelations.Clear();
            foreach (Entity e in EntityCache.All)
            {
                e.Refresh();
            }
        }

        internal void RegisterTypeRecursive(Type type)
        {
            RegisterType(type);
        }

        object ICloneable.Clone()
        {
            return Clone();
        }

        /// <summary>
        /// Creates a shallow copy of the session. Copies all except data.
        /// </summary>
        /// <returns></returns>
        public Session Clone()
        {
            Session s = new Session();
            s.dataProvider = DataProvider;
            s.pm = Pm.Clone();
            s.schema = Schema.Clone();
            return s;
        }
    }
}
